/*!
    \copyright  Copyright (c) 2024 Qualcomm Technologies International, Ltd.\n
                All Rights Reserved.\n
                Qualcomm Technologies International, Ltd. Confidential and Proprietary.
    \file
    \ingroup    usb_device
    \brief      Implementation of MS-USBEPD - Platform Detection Extensions.
*/

#include <usb.h>
#include <usb_device.h>
#include <usb_hub.h>
#include <usb_device_utils.h>
#include <stream.h>
#include <sink.h>
#include <source.h>
#include <stdlib.h>
#include "logging.h"

#ifdef ENABLE_USB_PLATDET

#define PLATDET_TIMEOUT 2500

static TaskData vendor_task;
static Sink vendor_sink;
static Source vendor_source;
static uint16 platdet_timeout = PLATDET_TIMEOUT;

/* The host MUST specify a USB Control Channel Message ID of 0xE0
 * for Host to Device Request messages, and the host MUST specify
 * a USB Control Channel Message ID of 0xE1 for Device to Host Response messages.
 */
#define MS_USBEPD_REQUEST_FROM_HOST 0xE0
#define MS_USBEPD_REQUEST_TO_HOST 0xE1

#define MS_PLATDE_COMMON_HEADER_SIZE 7

typedef enum
{
    /* Device Registration Message */
    USB_MS_PLATDE_CMD_REGISTER = 1,

    /* Platform Information Message */
    USB_MS_PLATDE_CMD_INFORMATION = 2,

    USB_MS_PLATDE_CMD_RESERVED = 0xFFFF
} usb_ms_platde_cmd_t;

typedef enum {
    USB_MS_PLATDE_MSG_INTERNAL_TIMEOUT,
  \
} usb_ms_platde_internal_msg_t;



#define MS_PLATDE_REGISTER_RESP_SIZE 9
#define MS_PLATDE_INFORMATION_REQ_SIZE 9
#define MS_PLATDE_INFORMATION_RESP_SIZE 7

typedef enum
{
    USB_MS_STATUS_NACK = 0x00,
    USB_MS_STATUS_ACK =  0x01
} usb_ms_platde_status_t;

uint16 platde_protocol_version;
usb_ms_platde_cmd_t platde_command;
uint8 platde_connection_id;
uint16 platde_sequence_number;

usb_platde_cb_t platde_callback;

static void usbDevice_Platform_Detected(usb_ms_platde_result_t result, usb_ms_platde_platform_t platform_id)
{
    if (platde_callback)
    {
        platde_callback(result, platform_id);
    }

    MessageCancelAll(&vendor_task, USB_MS_PLATDE_MSG_INTERNAL_TIMEOUT);
}

static int usbDevice_PlatDet_ProcessWrite(const uint8 *request_data,
        uint16 data_size, uint16 wValue)
{
    usb_ms_platde_status_t status;
    usb_ms_platde_platform_t platdef_platform;

    UNUSED(request_data);
    UNUSED(data_size);

    if (data_size < MS_PLATDE_COMMON_HEADER_SIZE)
    {
        /* Common Message Header length is 7 bytes */
        return -1;
    }

    /* process data_size bytes of request_data */
    status = request_data[0];
    platde_command = request_data[1] | (request_data[2] << 8);
    platde_connection_id = request_data[3];
    platde_sequence_number = request_data[5] | (request_data[6] << 8);

    platde_protocol_version = wValue;

    DEBUG_LOG_INFO("usb_platde: status enum:usb_ms_platde_status_t:%d"
                " command enum:usb_ms_platde_cmd_t:%d"
                " connection_id=%u seq=%u version=%u",
                status,
                platde_command,
                platde_connection_id, platde_sequence_number,
                platde_protocol_version);

    switch (platde_command)
    {
        case USB_MS_PLATDE_CMD_INFORMATION:
            if (data_size < MS_PLATDE_INFORMATION_REQ_SIZE)
            {
                DEBUG_LOG_ERROR("usb_platde: ERROR - length %u is not correct"
                                " for information command",       data_size);
                return -1;
            }

            platdef_platform = request_data[7] | (request_data[8] << 8);
            usbDevice_Platform_Detected(USB_MS_PLATDE_RESULT_SUCCESS, platdef_platform);

            break;
        default:
            break;
    }

    /* return SUCCESS */
    return 0;
}

static void usbDevice_PlatDet_ResponseHeader(uint8 *data)
{
    data[0] = USB_MS_STATUS_ACK;

    data[1] = platde_command & 0xff;
    data[2] = platde_command >> 8;

    data[3] = platde_connection_id;

    data[4] = 0;

    data[5] = platde_sequence_number & 0xff;
    data[6] = platde_sequence_number >> 8;

    data[7] = platde_protocol_version & 0xff;
    data[8] = platde_protocol_version >> 8;
}

static int usbDevice_PlatDet_ProcessRead(uint16 wLength)
{
    if (wLength < MS_PLATDE_COMMON_HEADER_SIZE)
    {
        return -1;
    }

    int data_len = -1;
    uint8 *data = NULL;

    switch (platde_command)
    {
        case USB_MS_PLATDE_CMD_REGISTER:
            data_len = MS_PLATDE_REGISTER_RESP_SIZE;
            data = (uint8 *)PanicUnlessMalloc(data_len);

            usbDevice_PlatDet_ResponseHeader(data);

            data[7] = platde_protocol_version & 0xff;
            data[8] = platde_protocol_version >> 8;
            break;

        case USB_MS_PLATDE_CMD_INFORMATION:
            data_len = MS_PLATDE_INFORMATION_RESP_SIZE;
            data = (uint8 *)PanicUnlessMalloc(data_len);

            usbDevice_PlatDet_ResponseHeader(data);
            break;
        default:
            break;
    }


    int copy_len = data_len;

    if (copy_len)
    {
        copy_len = MIN(copy_len, wLength);
        copy_len = MIN(copy_len, SinkSlack(vendor_sink));

        if (copy_len < data_len)
        {
            DEBUG_LOG_ERROR("usb_platde: vendor - response length %u"
                            "truncated to %u wLength=%u",
                            data_len, copy_len, wLength);
        }

        uint8 *ptr = SinkMapClaim(vendor_sink, copy_len);
        if (ptr)
        {
            memcpy(ptr, data, copy_len);
        }
        else
        {
            copy_len = -1;
        }
    }

    if (data)
    {
        free(data);
    }

    return copy_len;
}

static void usbDevice_PlatDet_VendorHandler(Task task, MessageId id, Message message)
{
    switch (id)
    {
        case USB_DEVICE_ENUMERATED:
            DEBUG_LOG_WARN("usb_platde:: device enumerated, start 10 seconds timer");
            MessageCancelAll(task, USB_MS_PLATDE_MSG_INTERNAL_TIMEOUT);
            MessageSendLater(task, USB_MS_PLATDE_MSG_INTERNAL_TIMEOUT, NULL, platdet_timeout);
            return;

        case USB_MS_PLATDE_MSG_INTERNAL_TIMEOUT:
            DEBUG_LOG_WARN("usb_platde:: timer expired");
            usbDevice_Platform_Detected(USB_MS_PLATDE_RESULT_TIMEOUT,USB_MS_PLATDE_RESERVED);
            return;

        case MESSAGE_MORE_DATA:
            break;

        default:
            DEBUG_LOG_WARN("usb_platde:: unhandled id %d", id);
            return;
    }

    const MessageMoreData *msg = (const MessageMoreData *)message;
    Source source = msg->source;

    if (source != vendor_source)
    {
        DEBUG_LOG_WARN("usb_platde:: vendor - source 0x%x != vendor source 0x%x",
                source, vendor_source);
        return;
    }

    DEBUG_LOG_ALWAYS("usb_platde:: vendor request handler, source has %d bytes",
            SourceBoundary(source));

    uint16 data_size;

    while ((data_size = SourceBoundary(source)) != 0)
    {
        UsbResponse resp;
        /* copy the original request to the response */
        memcpy(&resp.original_request, SourceMapHeader(source), sizeof(UsbRequest));
        /* Set the response fields to default values to make the code below simpler */

        DEBUG_LOG_WARN("usb_platde:: vendor - bmRequestType=%x bRequest=%x wValue=%x wIndex=%x wLength=%x",
                resp.original_request.bmRequestType,
                resp.original_request.bRequest,
                resp.original_request.wValue,
                resp.original_request.wIndex,
                resp.original_request.wLength);

        resp.data_length = 0;
        resp.success = FALSE;

        int result = -1;

        if (resp.original_request.bRequest == MS_USBEPD_REQUEST_FROM_HOST &&
                USB_BMREQUEST_DIRECTION(resp.original_request.bmRequestType) ==
                                    USB_HOST_TO_DEVICE)
        {
            /* good request from the host */
            const uint8 *request_data = SourceMap(source);
            result = usbDevice_PlatDet_ProcessWrite(request_data, data_size,
                                                    resp.original_request.wValue);
        }
        else if (resp.original_request.bRequest == MS_USBEPD_REQUEST_TO_HOST &&
                USB_BMREQUEST_DIRECTION(resp.original_request.bmRequestType) ==
                                    USB_DEVICE_TO_HOST)
        {
            /* good request from the host */
            result = usbDevice_PlatDet_ProcessRead(resp.original_request.wLength);
        }

        if (result >= 0)
        {
            resp.data_length = result;
            resp.success = TRUE;
        }

        /* Send response */
        if (resp.data_length)
        {
            (void)SinkFlushHeader(vendor_sink,
                                  resp.data_length,
                                  (uint16 *)&resp, sizeof(UsbResponse));
        }
        else
        {
            /* Sink packets can never be zero-length, so flush a dummy byte */
            (void) SinkClaim(vendor_sink, 1);
            (void) SinkFlushHeader(vendor_sink, 1, (uint16 *) &resp, sizeof(UsbResponse));
        }

        /* Discard the original request */
        SourceDrop(source, data_size);
    }
}

void UsbDevice_EnablePlatDet(bool enable, usb_platde_cb_t callback, uint16 timeout)
{
    if (enable)
    {
        platdet_timeout = timeout;

        /* This is optional - if bcdVersion is set to 2.01 then MS OS 2.0
         * descriptors are used, otherwise MS OS 1.0. */
        if (!UsbHubConfigKey(USB_DEVICE_CFG_VERSION, 0x201))
        {
            Panic();
        }
        vendor_task.handler = usbDevice_PlatDet_VendorHandler;
        vendor_sink = StreamUsbVendorSink();
        vendor_source = StreamSourceFromSink(vendor_sink);
        MessageStreamTaskFromSink(vendor_sink, &vendor_task);
        (void)SourceConfigure(vendor_source, VM_SOURCE_MESSAGES, VM_MESSAGES_SOME);

        StreamConfigure(VM_STREAM_USB_VENDOR_PLATFORM_DETECTION_ENABLE, 1);

        platde_callback = callback;
        UsbDevice_ClientRegister(&vendor_task);

        DEBUG_LOG_ALWAYS("usb_platde:: register to receive vendor requests");
    }
    else
    {
        DEBUG_LOG_INFO("usb_platde:: vendor requests disable");
        MessageStreamTaskFromSink(vendor_sink, NULL);
        vendor_sink = 0;
        StreamConfigure(VM_STREAM_USB_VENDOR_PLATFORM_DETECTION_ENABLE, 0);

        platde_callback = NULL;
        MessageCancelAll(&vendor_task, USB_DEVICE_MESSAGE_END);
        UsbDevice_ClientUnregister(&vendor_task);
    }
}

#endif /* VM_STREAM_USB_VENDOR_PLATFORM_DETECTION_ENABLE */
